跳到主要内容

Java 并发编程-啥是线程

前置知识准备

一个 Java程序实际上至少有三个线程,主线程,GC线程,异常处理线程

Java 无法通过自己开启线程,它是通过调用本地方法来开启线程的

private native void start0();

不过多线程和单线程执行同个任务,单线程是要快过多线程的,因为 CPU切换也是要花时间的

并发与并行的区别

先来了解一个概念

并发:CPU一核,模拟出来多条线程(快速交替,分配时间片) 并行:CPU多核,多个核心执行多个任务

所以多线程也未必是调用多核

下面这个方法可以用来判断可运行处理器数量(cpu 核数)

public class Demo1 {
public static void main(String[] args) {
System.out.println(Runtime.getRuntime().availableProcessors());
}
}

借用知乎用户 并发与并行的区别是什么? - 「已注销」的回答 - 知乎 的回答

你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。 你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。 你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。

并发的关键是你有处理多个任务的能力,不一定要同时。 并行的关键是你有同时处理多个任务的能力。

所以它们最关键的点就是:是否是『同时』。

线程与进程有什么区别?

抽象点说:进程是资源分配的最小单位,线程是CPU调度的最小单位

进程就是动态的程序,而进程作为 资源分配的单位,系统在运行时会为每个进程分配不同的内存区域,进程之间是相互独立的,不共享内存和数据

如下图所示,在 windows 中通过查看任务管理器的方式,我们就可以清楚看到 window 当前运行的进程(.exe 文件的运行)。

线程则是 调度和执行的单位 每个线程拥有独立的运行栈和程序计数器,对于线程来说只有 CPU 里的东西是自己独享的。

又因为一个进程中的多个线程共享相同的内存单元、内存地址空间,所以它们可以从同一堆中分配对象,可以访问相同的变量和对象。

331425-20160623115846235-947282498.png

如图的 Java栈和程序计数器,每个线程都有一份;而方法区和堆则是全部线程共享的

守护线程和普通线程的区别

参考资料 守护线程与普通线程

守护进程(Daemon)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。也就是说守护线程不依赖于终端,但是依赖于系统,与系统 “同生共死”。

从字面上我们很容易将守护线程理解成是由虚拟机(virtual machine)在内部创建的,而用户线程则是自己所创建的。事实并不是这样,任何线程都可以是 “守护线程 Daemon” 或 “用户线程 User”。他们在几乎每个方面都是相同的,唯一的区别是判断虚拟机何时离开

守护线程与普通线程的唯一区别是:当 JVM 中所有的线程都是守护线程的时候,JVM 就可以退出了;如果还有一个或以上的非守护线程则不会退出。(以上是针对正常退出,调用 System.exit 则必定会退出)

将一个用户线程设置为守护线程的方式是在线程对象创建之前调用线程对象的 setDaemon 方法

public class Test extends Thread {
public void run() {
for (int i = 0 ; i < 100 ; i ++) {
try {
Thread.sleep(100) ;
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(i);
}
}

// 如下创建一个守护进程
public static void main (String args[]) {
Test test = new Test() ;
test.setDaemon(true) ;
test.start() ;
System.out.println("isDaemon=" + test.isDaemon());

try {
// 等待用户输入,当用户输入之后就会直接结束,而无需等待守护进程运行完
System.in.read() ;
} catch (IOException e) {
e.printStackTrace();
}
}
}

所以 setDeamon(true) 的唯一意义就是告诉 JVM 不需要等待它退出,让 JVM 喜欢什么退出就退出吧,不用管它。

守护线程在没有用户线程可服务时自动离开,在Java中比较特殊的线程是被称为守护(Daemon)线程的低级别线程。这个线程具有最低的优先级,用于为系统中的其它对象和线程提供服务。

例如 Java 垃圾回收线程就是一个典型的守护线程,Java 垃圾回收线程始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源,当我们的程序中不再有任何运行中的 Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是 Java 虚拟机上仅剩的线程时,Java 虚拟机会自动离开。

上下文切换

参考资料 第一章 进程与线程的基本概念

上下文切换(有时也称做进程切换或任务切换)是指 CPU 从一个进程(或线程)切换到另一个进程(或线程)。上下文是指某一时间点 CPU 寄存器和程序计数器的内容。

寄存器是 cpu 内部的少量的速度很快的闪存,通常存储和访问计算过程的中间值提高计算机程序的运行速度。

程序计数器是一个专用的寄存器,用于表明指令序列中 CPU 正在执行的位置,存的值为正在执行的指令的位置或者下一个将要被执行的指令的位置,具体实现依赖于特定的系统。

举例说明 线程 A - B

  1. 先挂起线程 A,将其在 cpu 中的状态保存在内存中。
  2. 在内存中检索下一个线程 B 的上下文并将其在 CPU 的寄存器中恢复,执行B线程。
  3. 当 B 执行完,根据程序计数器中指向的位置恢复线程A。

CPU通过为每个线程分配CPU时间片来实现多线程机制。CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。

但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换。

上下文切换通常是计算密集型的,意味着此操作会 消耗大量的 CPU 时间,故线程也不是越多越好。如何减少系统中上下文切换次数,是提升多线程性能的一个重点课题。

线程6个状态

参考资料 第四章 Java线程的状态及主要转化方法

在现在的操作系统中,线程是被视为轻量级进程的,所以操作系统线程的状态其实和操作系统进程的状态是一致的。

操作系统线程主要有以下三个状态:

  • 就绪状态(ready):线程正在等待使用CPU,经调度程序调用之后可进入running状态。
  • 执行状态(running):线程正在使用CPU。
  • 等待状态(waiting): 线程经过等待事件的调用或者正在等待其他资源(如I/O)。

通过 getState() 可以获取这个线程的当前状态

while (mThread.getState() != Thread.State.TERMINATED) {
Thread.sleep(100);
}

上面的 Thread.State 枚举内部包含如下 6个状态

public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
状态码描述
NEW(新建)尚未启动的线程处于这种状态。
RUNNABLE(运行)运行中的线程,正在执行 run() 方法的Java代码;
BLOCKED(阻塞)运行中的线程,因为某些操作(synchronized)被阻塞而挂起;
WAITING(等待)运行中的线程,因为某些操作(例如等待输入)在等待中;
TIMED_WAITING(超时等待)运行中的线程,因为执行 sleep() 方法正在计时等待;
TERMINATED(死亡)线程已终止,因为 run() 方法执行完毕

注意图里的 READY 和 RUNNING 都属于运行状态 RUNNABLE,区别在于是否取得了时间片

graph TB 创建状态#NEW --启动线程--> 就绪状态#READY 阻塞状态#BLOCKED --阻塞解除--> 就绪状态#READY 就绪状态#READY --获得CPU资源---> 运行状态#RUNNING 运行状态#RUNNING --释放得CPU资源--> 就绪状态#READY 运行状态#RUNNING --等待用户输入或线程休眠等---> 阻塞状态#BLOCKED 运行状态#RUNNING --线程自然执行完毕或外部干涉终止线程---> 死亡状态#TERMINATED

NEW

处于 NEW 状态的线程此时尚未启动。这里的尚未启动指的是还没调用 Thread 实例的 start() 方法。

private void testStateNew() {
Thread thread = new Thread(() -> {});
System.out.println(thread.getState()); // 输出 NEW
}

从上面可以看出,只是创建了线程而并没有调用 start() 方法,此时线程处于 NEW 状态。

RUNNABLE

表示当前线程正在运行中。处于 RUNNABLE 状态的线程在 Java 虚拟机中运行,也有可能在等待 CPU 分配资源。

Java 线程的 RUNNABLE 状态其实是包括了传统操作系统线程的 ready 和 running 两个状态的(区别在于是否取得了时间片)。

BLOCKED

阻塞状态。处于 BLOCKED 状态的线程正等待锁的释放以进入同步区。

WAITING

等待状态。处于等待状态的线程变成 RUNNABLE 状态需要其他线程唤醒。 调用如下3个方法会使线程进入等待状态:

Object.wait():使当前线程处于等待状态直到另一个线程唤醒它; Thread.join():等待线程执行完毕,底层调用的是 Object 实例的 wait 方法; LockSupport.park():除非获得调用许可,否则禁用当前线程进行线程调度。

TIMED_WAITING

超时等待状态。线程等待一个具体的时间,时间到后会被自动唤醒。

调用如下方法会使线程进入超时等待状态:

Thread.sleep(long millis):使当前线程睡眠指定时间; Object.wait(long timeout):线程休眠指定时间,等待期间可以通过 notify()/notifyAll() 唤醒; Thread.join(long millis):等待当前线程最多执行 millis 毫秒,如果 millis 为 0,则会一直执行; LockSupport.parkNanos(long nanos): 除非获得调用许可,否则禁用当前线程进行线程调度指定时间; LockSupport.parkUntil(long deadline):同上,也是禁止线程进行调度指定时间;

当时间到了,就自动唤醒,拥有了去争夺锁的资格。

TERMINATED

终止状态。此时线程已执行完毕。

线程状态的转换

下面有详细的执行哪些方法会强制切换到其它状态

image.png